package middleware

import (
	"encoding/json"
	"fmt"
	"github.com/gofiber/fiber/v2"
	"go-web2/app/common/config"
	"go-web2/app/common/mylog"
	"go-web2/app/common/util"
	model "go-web2/app/model/sys"
	"net"
	"net/http"
	"os"
	"regexp"
	"strings"
	"time"
)

// ================================================= 中间件合集 =================================================

var apis = config.RouteApi

// 统一的日志格式化输出中间件
func LoggerPrint() fiber.Handler {
	return func(c *fiber.Ctx) error {
		start := time.Now()
		// 处理请求
		err := c.Next()
		var logMessage string
		if err != nil {
			// 记录日志
			logMessage = fmt.Sprintf("[%s] %s %s - %s ==> [Error] %s\n", start.Format("2006-01-02 15:04:05"), c.Method(), c.Path(), time.Since(start), err.Error())
		} else {
			// 记录日志
			logMessage = fmt.Sprintf("[%s] %s %s - %s\n", start.Format("2006-01-02 15:04:05"), c.Method(), c.Path(), time.Since(start))
		}
		// 输出到控制台
		fmt.Print(logMessage)
		// 输出到文件
		filename := "logs/" + time.Now().Format("2006-01-02") + ".log"
		file, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
		if err != nil {
			mylog.Error("日志文件的打开错误 : " + err.Error())
			return err
		}
		defer file.Close()
		if _, err := file.WriteString(logMessage); err != nil {
			mylog.Error("写入日志文件错误 : " + err.Error())
		}
		return err
	}
}

// token校验
func CheckToken(c *fiber.Ctx) error {
	parsedIP := c.IP() // 获取用户请求的ip
	// 校验用户 IP 是否在白名单中
	if !isIPInWhitelist(parsedIP) {
		return c.Status(http.StatusOK).JSON(config.Error("非法访问"))
	}
	path := c.Path() // 这里获取的是接口的完整路径，如：/sys/login、/sys/user/list
	// 排除指定接口，不校验token
	if path == "/sys/login" || path == "/sys/getKey" || path == "/sys/getCode" {
		return c.Next()
	}
	// 获取请求头中的 Token，并校验
	token, err := model.GetToken(c) // 在GetToken方法中，就已经校验了token了
	if err != nil {
		return c.Status(http.StatusOK).JSON(err)
	}
	// 鉴权（接口菜单鉴权）
	if !checkPermisson(c, token) {
		return c.Status(http.StatusOK).JSON(config.Error("没有操作权限"))
	}
	// 设置请求头
	setHeader(c)
	// 刷新token有效期刷新和定期刷新
	refreshToken(c, token)
	// 因为所有接口（除了上面排除的三个接口），都需要经过这个中间件，然后这个中间件获取token时，已经解析、检验过token了
	// 所以这里直接将解析并且校验通过的token重新设置到请求头中，当那些接口去拿请求头的token时，直接拿，不用再进行解析校验。
	c.Request().Header.Set(config.TokenHeader, token)
	return c.Next()
}

// 辅助函数：检查 IP 是否在白名单中
func isIPInWhitelist(ip string) bool {
	parsedIP := net.ParseIP(ip)
	for _, allowedIP := range config.AuthHost {
		if allowedIP == "*" {
			return true
		}
		if strings.Contains(allowedIP, "*") {
			// 将 * 转换为正则表达式
			regexPattern := strings.ReplaceAll(allowedIP, "*", ".*")
			if match, _ := regexp.MatchString("^"+regexPattern+"$", ip); match {
				return true
			}
		} else {
			// 非通配符的精确匹配
			if parsedIP.Equal(net.ParseIP(allowedIP)) {
				return true
			}
		}
	}
	return false
}

// 辅助函数：鉴权（接口菜单鉴权）
func checkPermisson(c *fiber.Ctx, token string) bool {
	path := c.Path() // 这里获取的是接口的完整路径，如：/sys/login、/sys/user/list
	flag := true
	api := apis[path]
	if api.Permission != "" {
		user := model.GetLoginUser(token)
		permList := model.GetPermList(user.RoleId)
		split := strings.Split(api.Permission, ";")
		flag = false
		for i := range split {
			if util.IsContain(permList, split[i]) {
				flag = true
				break
			}
		}
	}
	return flag
}

// 辅助函数：刷新token有效期刷新和定期刷新
func refreshToken(c *fiber.Ctx, token string) {
	// 刷新有效期
	v := model.GetCreateTime(token) // 获取token的创建时间
	//判断token的创建时间是否大于2小时，如果是的话则需要刷新token
	s := time.Now().Unix() - v
	hour := s / 1000 / (60 * 60)
	if hour > 2 {
		// TODO 获取当前用户信息，重新登录，生成新的token，将新token设置到响应头中
		user := model.GetLoginUser(token)
		expire := model.GetExpire(token)
		// TODO 这里重新登录（会把旧登录删除），生成新的token
		splits := strings.Split(token, "_")
		var newToken string
		if len(splits) > 1 {
			newToken = user.Login(splits[0], expire)
		} else {
			newToken = user.Login("", expire)
		}
		token = newToken
		c.Response().Header.Set(config.TokenHeader, newToken) // 将新的token设置到请求头中
	}
	timeOut := model.GetTimeOut(token) // 获取token的过期时间
	if timeOut != -1 {                 // token没过期，过期时间不是-1的时候，每次请求都刷新过期时间
		model.UpdateTimeOut(token, config.TokenExpire)
	}
}

// 设置请求头
func setHeader(c *fiber.Ctx) {
	// 校验Origin值
	origin := c.Get("Origin")
	if origin != "" && util.IsContain(config.AllowedOrigins, origin) {
		c.Set("Access-Control-Allow-Origin", origin)
	} else {
		c.Set("Access-Control-Allow-Origin", "")
	}
	c.Set("Set-Cookie", "name=value; SameSite=Strict; cookiename=httponlyTest;Path=/;Domain=domainvalue;Max-Age=seconds;Secure;HTTPOnly")
	c.Set("Content-Security-Policy", "default-src 'self'; script-src 'self'; frame-ancestors 'self'; object-src 'none'")
	c.Set("Access-Control-Allow-Credentials", "true")
	c.Set("Referrer-Policy", "no-referrer")
	c.Set("X-XSS-Protection", "1; mode=block") //1; mode=block：启用XSS保护，并在检查到XSS攻击时，停止渲染页面
	c.Set("X-Content-Type-Options", "nosniff") //互联网上的资源有各种类型，通常浏览器会根据响应头的Content-Type字段来分辨它们的类型。通过这个响应头可以禁用浏览器的类型猜测行为
	c.Set("X-Frame-Options", "SAMEORIGIN")     //SAMEORIGIN：不允许被本域以外的页面嵌入
	c.Set("X-DNS-Prefetch-Control", "off")
	c.Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
	c.Set("Cache-Control", "no-cache, no-store, must-revalidate")
	c.Set("Pragma", "no-cache")
	c.Set("Expires", "0")
}

// 保存日志到数据库：操作日志
func SysLogInit(c *fiber.Ctx) error {
	path := c.Path() // 获取当前请求的路径
	// 跳过get请求
	if c.Method() == fiber.MethodGet || strings.Contains(path, "/sys/login") {
		return c.Next()
	}
	split := strings.Split(path, "/") // path根据 / 分割
	var entity model.SysLog
	api := apis[path]
	entity.IP = c.IP() // 获取用户IP
	entity.Title = api.Description
	entity.Type = getLogType(api.Description)
	entity.Method = split[len(split)-1]
	entity.Url = path // 获取当前请求的路径
	entity.Info = entity.Title + "成功"
	entity.State = "操作成功"
	// 调用下一个中间件或路由处理程序，用来获取响应给前端的数据
	c.Next()
	code := c.Response().StatusCode()
	if code != 200 {
		entity.State = "操作失败"
		entity.Info = "未知异常"
	} else {
		var result config.Result
		json.Unmarshal(c.Response().Body(), &result)
		if result.Code != 0 {
			entity.State = "操作失败"
			entity.Info = entity.Title + "失败：" + result.Message
		}
	}
	token := c.Get(config.TokenHeader)
	user := model.GetLoginUser(token)
	entity.CreatorId = &user.UserName
	entity.Info = user.UserName + " " + entity.Info
	entity.Insert()
	return c.Next()
}

// 辅助函数：根据接口获取接口类型
func getLogType(description string) string {
	// 定义要匹配的关键字列表
	keywords := []string{"登录", "退出", "新增", "修改", "删除", "上传", "导入", "设置密码", "重置密码"}
	// 构建正则表达式模式，使用或操作符将关键字列表组合起来
	pattern := fmt.Sprintf("(%s)", strings.Join(keywords, "|"))
	// 编译正则表达式模式
	regex := regexp.MustCompile(pattern)
	match := regex.FindStringSubmatch(description)
	logType := "其他"
	if match != nil {
		logType = match[0]
	}
	return logType
}
